/*

 Copyright 2012 Peter Dornbach.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
   
*/

package com.dornbachs.zebra.guitarear;

import java.util.Vector;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;

public class Riff {
	public static Riff singleChordRiff(ChordShape shape) {
		Riff riff = new Riff(120);
		riff.strumChord(shape);
		return riff;
	}
	
	public Riff(int bpm) {
		_bpm = bpm;
	}
	
	public void strumChord(ChordShape shape) {
		if (!_playing) {
			Strum strum = currentStrum();
			for (String note : shape._notes) {
				strum.addNote(note);
			}
		}
	}

	public synchronized void strumNote(String note) {
		if (!_playing) {
			currentStrum().addNote(note);
		}
	}

	public void advance(float beats) {
		if (!_playing) {
			_beats = beats;
		}
	}

	public void mute() {
		if (!_playing) {
			// A new strum with or without any notes mutes the previous.
			currentStrum();
		}
	}

	public boolean play(SoundPlayer.Instrument instrument) {
		if (!_globalPlaying) {
			_playing = true;
			_globalPlaying = true;
	
			Handler h = new Handler() {
				@Override
				public void handleMessage(Message m) {
					_playing = false;
					_globalPlaying = false;
				}
			};
	
			new Player(instrument, _strums, _bpm, h);
			return true;
		} else {
			return false;
		}
	}

	private Strum currentStrum() {
		Strum currentStrum = null;
		if (_strums.size() > 0) {
			currentStrum = _strums.lastElement();
		}
		if (currentStrum == null || _beats > currentStrum._beat) {
			currentStrum = new Strum(_beats);
			_strums.add(currentStrum);
		}
		return currentStrum;
	}

	// A strum mutes everything previously playing.
	private static final class Strum {
		public Strum(float beat) {
			_beat = beat;
		}

		public void addNote(String note) {
			if (_notes == null) {
				_notes = new Vector<String>(MAX_CHANNELS);
			}
			if (_notes.size() < MAX_CHANNELS) {
				_notes.add(note);
			}
		}

		public float _beat;
		public Vector<String> _notes;
	};

	private final static class Player {
		public Player(SoundPlayer.Instrument instrument, Vector<Strum> strums,
				int bpm, Handler playbackFinishedHandler) {
			_instrument = instrument;
			_strums = strums;
			_millisPerBeat = 60 * 1000 / bpm;
			_beatsPerMilli = 1.0f / _millisPerBeat;
			_startTimeMillis = SystemClock.uptimeMillis();
			_playbackFinishedHandler = playbackFinishedHandler;

			Thread t = new Thread(new Runnable() {
				public void run() {
					for(;;) {
						long w = tick();
						if (w > 0) {
							try {
								Thread.sleep(w);
							}
							catch (InterruptedException e) {
							}
						} else {
							_playbackFinishedHandler.sendMessage(Message.obtain());
							return;
						}
					}
				}
			});
			t.setPriority(Thread.MAX_PRIORITY);
			t.start();
		}

		private long tick() {
			final float TOLERANCE = 0.001f;

			long t = SystemClock.uptimeMillis();
			float currentBeat = (t - _startTimeMillis) * _beatsPerMilli;
			Strum nextStrum = (_nextStrumIdx < _strums.size() ? _strums.get(_nextStrumIdx)
					: null);
			if (nextStrum != null && currentBeat > nextStrum._beat - TOLERANCE) {
				_instrument.mute();
				boolean strummedAnyNotes = false;
				if (nextStrum._notes != null) {
					for (String note : nextStrum._notes) {
						_instrument.play(note);
						strummedAnyNotes = true;
					}
				}
				_lastStrumMillis = (strummedAnyNotes ? t : 0);
				_nextStrumIdx++;
			} else {
				if (_lastStrumMillis > 0) {
					long fadeoutEndMillis = _lastStrumMillis + FADEOUT_START_MILLIS
							+ FADEOUT_LENGTH_MILLIS;
					float volume = (float) (fadeoutEndMillis - t) / FADEOUT_LENGTH_MILLIS;
					if (volume > 1.0f) {
						_instrument.setVolume(1.0f);
					} else if (volume < 0.0f) {
						_instrument.setVolume(0.0f);
					} else {
						_instrument.setVolume(volume);
					}
				}
			}

			long nextStrumEventMillis = nextStrumEventMillis();
			long nextVolumeEventMillis = nextVolumeEventMillis(t);
			long nextEventMillis = 0;
			if (nextStrumEventMillis > 0) {
				if (nextVolumeEventMillis > 0) {
					nextEventMillis = Math.min(nextStrumEventMillis, nextStrumEventMillis);
				} else {
					nextEventMillis = nextStrumEventMillis;
				}
			} else {
				nextEventMillis = nextVolumeEventMillis;
			}
			if (nextEventMillis > 0) {
				return nextEventMillis - t;
			} else {
				return 0;
			}
		}

		private final long nextStrumEventMillis() {
			if (_nextStrumIdx < _strums.size()) {
				return _startTimeMillis
						+ (int) (_strums.get(_nextStrumIdx)._beat * _millisPerBeat);
			} else {
				return 0;
			}
		}

		private final long nextVolumeEventMillis(long t) {
			if (_lastStrumMillis > 0) {
				long fadeoutStartMillis = _lastStrumMillis + FADEOUT_START_MILLIS;
				if (t < fadeoutStartMillis) {
					return fadeoutStartMillis;
				} else {
					long fadeoutEndMillis = fadeoutStartMillis + FADEOUT_LENGTH_MILLIS;
					if (t < fadeoutEndMillis) {
						long nextVolumeEventMillis = t + FADEOUT_STEP_MILLIS;
						return (nextVolumeEventMillis < fadeoutEndMillis ? nextVolumeEventMillis
								: fadeoutEndMillis);
					}
				}
			}
			return 0;
		}

		private static final int FADEOUT_START_MILLIS = 600;
		private static final int FADEOUT_LENGTH_MILLIS = 600;
		private static final int FADEOUT_STEP_MILLIS = 100;

		private SoundPlayer.Instrument _instrument;
		private Vector<Strum> _strums;
		private int _millisPerBeat;
		private float _beatsPerMilli;
		private long _startTimeMillis;

		private long _lastStrumMillis;
		private int _nextStrumIdx;
		
		private Handler _playbackFinishedHandler;
	};

	final static int MAX_CHANNELS = 6;

	private static boolean _globalPlaying;
	private boolean _playing;
	private float _beats;
	private int _bpm;
	private Vector<Strum> _strums = new Vector<Strum>(16);
}
